﻿using System;
using System.Windows;
using System.Diagnostics;
using System.Windows.Media;
using System.Windows.Controls;
using System.Collections.Specialized;
using System.Windows.Controls.Primitives;

// code from series of articles: http://blogs.msdn.com/b/dancre/archive/2006/02/16/implementing-a-virtualizingpanel-part-4-the-goods.aspx
namespace Red.Controls
{
    public class VirtualizingTilePanel : VirtualizingPanel, IScrollInfo
    {
        #region Data

        private TranslateTransform _trans = new TranslateTransform();
        private ScrollViewer _owner;
        private bool _canHScroll = false;
        private bool _canVScroll = false;
        private Size _extent = new Size( 0, 0 );
        private Size _viewport = new Size( 0, 0 );
        private Point _offset;

        #endregion Data

        #region Properties

        // Dependency property that controls the size of the child elements
        public static readonly DependencyProperty ChildSizeProperty
           = DependencyProperty.RegisterAttached( "ChildSize", typeof( Size ), typeof( VirtualizingTilePanel ),
              new FrameworkPropertyMetadata( new Size( 200, 200 ), FrameworkPropertyMetadataOptions.AffectsMeasure |
              FrameworkPropertyMetadataOptions.AffectsArrange ) );

        // Accessor for the child size dependency property
        public Size ChildSize
        {
            get { return ( Size )GetValue( ChildSizeProperty ); }
            set { SetValue( ChildSizeProperty, value ); }
        }

        #endregion Properties

        #region C-tor

        public VirtualizingTilePanel()
        {
            // For use in the IScrollInfo implementation
            RenderTransform = _trans;
        }

        #endregion C-tor

        #region FrameworkElement overrides

        /// <summary>
        /// Measure the children
        /// </summary>
        /// <param name="availableSize">Size available</param>
        /// <returns>Size desired</returns>
        protected override Size MeasureOverride( Size availableSize )
        {
            UpdateScrollInfo( availableSize );

            // Figure out range that's visible based on layout algorithm
            int firstVisibleItemIndex, lastVisibleItemIndex;
            GetVisibleRange( out firstVisibleItemIndex, out lastVisibleItemIndex );

            // We need to access InternalChildren before the generator to work around a bug
            UIElementCollection children = InternalChildren;
            IItemContainerGenerator generator = ItemContainerGenerator;

            // Get the generator position of the first visible data item
            GeneratorPosition startPos = generator.GeneratorPositionFromIndex( firstVisibleItemIndex );

            // Get index where we'd insert the child for this position. If the item is realized
            // (position.Offset == 0), it's just position.Index, otherwise we have to add one to
            // insert after the corresponding child
            int childIndex = ( startPos.Offset == 0 ) ? startPos.Index : startPos.Index + 1;

            using ( generator.StartAt( startPos, GeneratorDirection.Forward, true ) )
            {
                for ( int itemIndex = firstVisibleItemIndex; itemIndex <= lastVisibleItemIndex; ++itemIndex, ++childIndex )
                {
                    bool newlyRealized;

                    // Get or create the child
                    UIElement child = generator.GenerateNext( out newlyRealized ) as UIElement;
                    if ( newlyRealized )
                    {
                        // Figure out if we need to insert the child at the end or somewhere in the middle
                        if ( childIndex >= children.Count )
                        {
                            base.AddInternalChild( child );
                        }
                        else
                        {
                            base.InsertInternalChild( childIndex, child );
                        }
                        generator.PrepareItemContainer( child );
                    }
                    else
                    {
                        // The child has already been created, let's be sure it's in the right spot
                        Debug.Assert( child == children[ childIndex ], "Wrong child was generated" );
                    }

                    // Measurements will depend on layout algorithm
                    child.Measure( ChildSize );
                }
            }

            // Note: this could be deferred to idle time for efficiency
            CleanUpItems( firstVisibleItemIndex, lastVisibleItemIndex );

            return availableSize;
        }

        /// <summary>
        /// Arrange the children
        /// </summary>
        /// <param name="finalSize">Size available</param>
        /// <returns>Size used</returns>
        protected override Size ArrangeOverride( Size finalSize )
        {
            IItemContainerGenerator generator = ItemContainerGenerator;

            UpdateScrollInfo( finalSize );

            for ( int i = 0; i < Children.Count; i++ )
            {
                UIElement child = Children[ i ];

                // Map the child offset to an item offset
                int itemIndex = generator.IndexFromGeneratorPosition( new GeneratorPosition( i, 0 ) );

                ArrangeChild( itemIndex, child, finalSize );
            }

            return finalSize;
        }

        #endregion FrameworkElement overrides

        #region VirtualizingPanel overrides

        /// <summary>
        /// When items are removed, remove the corresponding UI if necessary
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="args"></param>
        protected override void OnItemsChanged( object sender, ItemsChangedEventArgs args )
        {
            switch ( args.Action )
            {
                case NotifyCollectionChangedAction.Reset:
                    SetVerticalOffset( 0 );
                    break;
                case NotifyCollectionChangedAction.Remove:
                case NotifyCollectionChangedAction.Replace:
                case NotifyCollectionChangedAction.Move:
                    RemoveInternalChildRange( args.Position.Index, args.ItemUICount );
                    break;
            }
        }

        protected override void BringIndexIntoView( int index )
        {
            int childrenPerRow = CalculateChildrenPerRow( _extent );
            SetVerticalOffset( index * ChildSize.Height / childrenPerRow );
        }

        #endregion VirtualizingPanel overrides

        #region Private methods
        
        /// <summary>
        /// Revirtualize items that are no longer visible
        /// </summary>
        /// <param name="minDesiredGenerated">first item index that should be visible</param>
        /// <param name="maxDesiredGenerated">last item index that should be visible</param>
        private void CleanUpItems( int minDesiredGenerated, int maxDesiredGenerated )
        {
            UIElementCollection children = InternalChildren;
            IItemContainerGenerator generator = ItemContainerGenerator;

            for ( int i = children.Count - 1; i >= 0; i-- )
            {
                GeneratorPosition childGeneratorPos = new GeneratorPosition( i, 0 );
                int itemIndex = generator.IndexFromGeneratorPosition( childGeneratorPos );
                if ( itemIndex < minDesiredGenerated || itemIndex > maxDesiredGenerated )
                {
                    generator.Remove( childGeneratorPos, 1 );
                    RemoveInternalChildRange( i, 1 );
                }
            }
        }

        #endregion Private methods

        #region Layout specific code

        // I've isolated the layout specific code to this region. If you want to do something other than tiling, this is
        // where you'll make your changes

        /// <summary>
        /// Calculate the extent of the view based on the available size
        /// </summary>
        /// <param name="availableSize">available size</param>
        /// <param name="itemCount">number of data items</param>
        /// <returns></returns>
        private Size CalculateExtent( Size availableSize, int itemCount )
        {
            int childrenPerRow = CalculateChildrenPerRow( availableSize );

            // See how big we are
            return new Size( childrenPerRow * ChildSize.Width,
                ChildSize.Height * Math.Ceiling( ( double )itemCount / childrenPerRow ) );
        }

        /// <summary>
        /// Get the range of children that are visible
        /// </summary>
        /// <param name="firstVisibleItemIndex">The item index of the first visible item</param>
        /// <param name="lastVisibleItemIndex">The item index of the last visible item</param>
        private void GetVisibleRange( out int firstVisibleItemIndex, out int lastVisibleItemIndex )
        {
            int childrenPerRow = CalculateChildrenPerRow( _extent );

            firstVisibleItemIndex = ( int )Math.Floor( _offset.Y / ChildSize.Height ) * childrenPerRow;
            lastVisibleItemIndex = ( int )Math.Ceiling( ( _offset.Y + _viewport.Height ) / ChildSize.Height ) * childrenPerRow - 1;

            ItemsControl itemsControl = ItemsControl.GetItemsOwner( this );
            int itemCount = itemsControl.HasItems ? itemsControl.Items.Count : 0;
            if ( lastVisibleItemIndex >= itemCount )
                lastVisibleItemIndex = itemCount - 1;

        }

        /// <summary>
        /// Position a child
        /// </summary>
        /// <param name="itemIndex">The data item index of the child</param>
        /// <param name="child">The element to position</param>
        /// <param name="finalSize">The size of the panel</param>
        private void ArrangeChild( int itemIndex, UIElement child, Size finalSize )
        {
            int childrenPerRow = CalculateChildrenPerRow( finalSize );

            int row = itemIndex / childrenPerRow;
            int column = itemIndex % childrenPerRow;

            child.Arrange( new Rect( column * ChildSize.Width, row * ChildSize.Height, ChildSize.Width, ChildSize.Height ) );
        }

        /// <summary>
        /// Helper function for tiling layout
        /// </summary>
        /// <param name="availableSize">Size available</param>
        /// <returns></returns>
        private int CalculateChildrenPerRow( Size availableSize )
        {
            // Figure out how many children fit on each row
            int childrenPerRow;
            if ( availableSize.Width == Double.PositiveInfinity )
                childrenPerRow = Children.Count;
            else
                childrenPerRow = Math.Max( 1, ( int )Math.Floor( availableSize.Width / ChildSize.Width ) );
            return childrenPerRow;
        }

        #endregion Layout specific code

        #region IScrollInfo implementation

        // See Ben Constable's series of posts at http://blogs.msdn.com/bencon/

        private void UpdateScrollInfo( Size availableSize )
        {
            // See how many items there are
            ItemsControl itemsControl = ItemsControl.GetItemsOwner( this );
            int itemCount = itemsControl.HasItems ? itemsControl.Items.Count : 0;

            Size extent = CalculateExtent( availableSize, itemCount );
            // Update extent
            if ( extent != _extent )
            {
                _extent = extent;
                if ( _owner != null )
                    _owner.InvalidateScrollInfo();
            }

            // Update viewport
            if ( availableSize != _viewport )
            {
                _viewport = availableSize;
                if ( _owner != null )
                    _owner.InvalidateScrollInfo();
            }
        }

        public ScrollViewer ScrollOwner
        {
            get { return _owner; }
            set { _owner = value; }
        }

        public bool CanHorizontallyScroll
        {
            get { return _canHScroll; }
            set { _canHScroll = value; }
        }

        public bool CanVerticallyScroll
        {
            get { return _canVScroll; }
            set { _canVScroll = value; }
        }

        public double HorizontalOffset
        {
            get { return _offset.X; }
        }

        public double VerticalOffset
        {
            get { return _offset.Y; }
        }

        public double ExtentHeight
        {
            get { return _extent.Height; }
        }

        public double ExtentWidth
        {
            get { return _extent.Width; }
        }

        public double ViewportHeight
        {
            get { return _viewport.Height; }
        }

        public double ViewportWidth
        {
            get { return _viewport.Width; }
        }

        public void LineUp()
        {
            SetVerticalOffset( VerticalOffset - 10 );
        }

        public void LineDown()
        {
            SetVerticalOffset( VerticalOffset + 10 );
        }

        public void PageUp()
        {
            SetVerticalOffset( VerticalOffset - _viewport.Height );
        }

        public void PageDown()
        {
            SetVerticalOffset( VerticalOffset + _viewport.Height );
        }

        public void MouseWheelUp()
        {
            SetVerticalOffset( VerticalOffset - 20 );
        }

        public void MouseWheelDown()
        {
            SetVerticalOffset( VerticalOffset + 20 );
        }

        public void LineLeft()
        {
            throw new InvalidOperationException();
        }

        public void LineRight()
        {
            throw new InvalidOperationException();
        }

        public Rect MakeVisible( Visual visual, Rect rectangle )
        {
            return new Rect();
        }

        public void MouseWheelLeft()
        {
            throw new InvalidOperationException();
        }

        public void MouseWheelRight()
        {
            throw new InvalidOperationException();
        }

        public void PageLeft()
        {
            throw new InvalidOperationException();
        }

        public void PageRight()
        {
            throw new InvalidOperationException();
        }

        public void SetHorizontalOffset( double offset )
        {
        }

        public void SetVerticalOffset( double offset )
        {
            if ( offset < 0 || _viewport.Height >= _extent.Height )
            {
                offset = 0;
            }
            else
            {
                if ( offset + _viewport.Height >= _extent.Height )
                {
                    offset = _extent.Height - _viewport.Height;
                }
            }

            _offset.Y = offset;

            if ( _owner != null )
                _owner.InvalidateScrollInfo();

            _trans.Y = -offset;

            // Force us to realize the correct children
            InvalidateMeasure();
        }

        #endregion IScrollInfo implementation
    }
}
